#include "User.h"

void User::init(int argc, char* argv[]) {
    logfile.open("log.txt", ios_base::app | ios_base::out);

    logfile << "----------------------------------------------------------------\n";

    time_t rawtime = time(0);
    logfile << "cs294-13 proj2 started " << asctime(localtime(&rawtime));

    eyeTheta = 1;
    eyePhi = 0;
    eyeDist = 5.0;

    lightTheta = -1;
    lightPhi = 0;
    lightDist = 16.0;

    FreeImage_Initialise();
    glutInit(&argc, argv);

    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
	glutInitWindowPosition(100,100);
	glutInitWindowSize(640,480);
	glutCreateWindow("cs294-13 proj2");

    //must init glew after glut initialization
    glewInit();

    //check extensions
    if (!glewIsSupported("GL_VERSION_2_0")) {
		printf("OpenGL 2.0 not supported.\n");
		exit(1);
	}

    if (!glutExtensionSupported("GL_EXT_texture_cube_map")) {
        printf("EXT_texture_cube_map not supported.\n");
        exit(1);
    }

    glEnable(GL_DEPTH_TEST);
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glEnable(GL_CULL_FACE);

    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

    //shader init
    vertexShader = compileShader("main.vert");
    fragmentShader = compileShader("main.frag");
    program = linkProgram(vertexShader, fragmentShader);

    //null out map pointers for now
    shadowMap = 0;
    environmentMap = 0;
    diffuseMap = 0;

    shadowKernelSize = 16;

    //config file
    char* configFilename = "config.txt";
    if (argc >= 2) configFilename = argv[1];
    useConfigFile(configFilename);
}

//macro for readability
#define PROCESS_OP(op_name) \
    if (!strcmp(op, #op_name)) { \
        op_ ## op_name ## (configfile); \
        continue; \
    }

void User::useConfigFile(const char *filename) {
    ifstream configfile(filename);
    char op[256];
    
    while (true) {
        configfile >> op;
        if (!configfile.good()) break;

        PROCESS_OP(environmentMap);
        PROCESS_OP(shadowMapSize);
        PROCESS_OP(shadowKernelSize);

        logfile << "Unrecognized op '" << op << "'\n";
    }

    configfile.close();

    if (!environmentMap) {
        printf("Must specify environment map in config file.\n");
        exit(1);
    }

    if (!shadowMap) {
        shadowMap = new ShadowMap(1024);
    }
}

void User::op_environmentMap(ifstream& configfile) {
    int diffuseMapSize;
    char filename[256], ext[256];
    configfile >> filename >> ext >> diffuseMapSize;

    environmentMap = new EnvironmentMap(filename, ext);
    diffuseMap = environmentMap->generateDiffuseMapY(diffuseMapSize);
}

void User::op_shadowMapSize(ifstream& configfile) {
    if (!shadowMap) {
        int size;
        configfile >> size;
        shadowMap = new ShadowMap(size);
    }
}

void User::op_shadowKernelSize(ifstream& configfile) {
    configfile >> shadowKernelSize;
}

string User::readTextFile(const char* filename) {
    stringbuf sb;
    ifstream infile(filename);
    infile.get(sb, -1);

    return sb.str();
}

void User::shaderInfolog(GLuint obj) {
    int length;
    glGetShaderiv(obj, GL_INFO_LOG_LENGTH, &length);

    if (length > 0) {
        char* infolog = new char[length];
        int count;
        glGetShaderInfoLog(obj, length, &count, infolog);
        logfile << infolog;
        delete[] infolog;
    }
}

void User::programInfolog(GLuint obj) {
    int length;
    glGetProgramiv(obj, GL_INFO_LOG_LENGTH, &length);

    if (length > 0) {
        char* infolog = new char[length];
        int count;
        glGetProgramInfoLog(obj, length, &count, infolog);
        logfile << infolog;
        delete[] infolog;
    }
}

GLuint User::compileShader(const char* filename) {
    string filenameString = string(filename);
    int extStart = filenameString.length() - 5;
    if (extStart < 0) return 0;

    string ext = filenameString.substr(extStart);
    GLuint result;
    if (ext == ".vert") {
        result = glCreateShader(GL_VERTEX_SHADER);
    } else if (ext == ".frag") {
        result = glCreateShader(GL_FRAGMENT_SHADER);
    } else {
        return 0;
    }

    string sourceCode = readTextFile(filename);
    const char* sourceCode_c_str = sourceCode.c_str();
    glShaderSource(result, 1, &sourceCode_c_str, NULL);
    glCompileShader(result);

    shaderInfolog(result);
    return result;
}

GLuint User::linkProgram(GLuint vertexShader, GLuint fragmentShader) {
    GLuint result = glCreateProgram();
    glAttachShader(result, vertexShader);
    glAttachShader(result, fragmentShader);
    glLinkProgram(result);
    
    programInfolog(result);
    return result;
}

void User::setPerspective() {
    // Reset the coordinate system before modifying
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	
	// Set the viewport to be the entire window
    glViewport(0, 0, w, h);
    glFrustum(-1, 1, -1, 1, 1, 1024);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    double eyex = sin(eyePhi) * sin(eyeTheta) * eyeDist;
    double eyey = cos(eyeTheta) * eyeDist;
    double eyez = cos(eyePhi) * sin(eyeTheta) * eyeDist;
	gluLookAt(eyex, eyey, eyez, 
		      0.0, 0.0, 0.0,
			  0.0, 1.0, 0.0);
}

void User::geometry() {
    GLfloat zero[4] = {0, 0, 0, 1.0};
    GLfloat half[4] = {0.5, 0.5, 0.5, 1.0};
    GLfloat one[4] = {1.0, 1.0, 1.0, 1.0};

    glMaterialfv(GL_FRONT, GL_DIFFUSE, one);
    glMaterialfv(GL_FRONT, GL_SPECULAR, zero);
    
    glBegin(GL_QUADS);
        glNormal3f(0, 1, 0);
        glVertex3f(-16, -2, -16);
        glVertex3f(-16, -2, 16);
        glVertex3f(16, -2, 16);
        glVertex3f(16, -2, -16);
    glEnd();

    glMaterialfv(GL_FRONT, GL_DIFFUSE, half);
    glMaterialfv(GL_FRONT, GL_SPECULAR, half);

    glBegin(GL_QUADS);
        glNormal3f(0, -1, 0);
        glVertex3f(-2, 8, -2);
        glVertex3f(2, 8, -2);
        glVertex3f(2, 8, 2);
        glVertex3f(-2, 8, 2);
    glEnd();

    glMaterialfv(GL_FRONT, GL_DIFFUSE, zero);
    glMaterialfv(GL_FRONT, GL_SPECULAR, one);

    glutSolidTorus(0.25, 1, 64, 64);
    glutSolidSphere(0.25, 64, 64);
    pushMatrixTranslated(0, 2, 2, GL_TEXTURE7);
        pushMatrixRotated(45, 0, 1, 0, GL_TEXTURE7);
            glMaterialfv(GL_FRONT, GL_DIFFUSE, half);
            glMaterialfv(GL_FRONT, GL_SPECULAR, half);
            glutSolidTorus(0.25, 1, 64, 64);
        popMatrix(GL_TEXTURE7);
    popMatrix(GL_TEXTURE7);

    glMaterialfv(GL_FRONT, GL_DIFFUSE, one);
    glMaterialfv(GL_FRONT, GL_SPECULAR, zero);

    pushMatrixTranslated(4, 0, 0, GL_TEXTURE7);
        glutSolidCube(2);
    popMatrix(GL_TEXTURE7);

    pushMatrixTranslated(-2, 4, -4, GL_TEXTURE7);
        glutSolidSphere(2, 64, 64);
    popMatrix(GL_TEXTURE7);
}

void User::render() {
    clock_t elapsedClock = clock() - lastFrame;
    float elapsedTime = float(elapsedClock) / CLOCKS_PER_SEC;
    lastFrame = clock();

    if (elapsedTime > 1) elapsedTime = 1;

    if (mouseButton == GLUT_LEFT_BUTTON) {
        eyePhi -= diffMouseX * 0.01;
        eyeTheta -= diffMouseY * 0.01;
        if (eyeTheta < 0.01) eyeTheta = 0.01;
        else if (eyeTheta > pi - 0.01) eyeTheta = pi - 0.01;
    } else if (mouseButton == GLUT_RIGHT_BUTTON) {
        lightPhi -= diffMouseX * 0.01;
        lightTheta -= diffMouseY * 0.01;
    }

    diffMouseX = 0;
    diffMouseY = 0;

    //render shadow map
    GLfloat lightpos[4];
    lightpos[0] = sin(lightPhi) * sin(lightTheta) * lightDist;
    lightpos[1] = cos(lightTheta) * lightDist;
    lightpos[2] = cos(lightPhi) * sin(lightTheta) * lightDist;
    lightpos[3] = 1.0;

    glLightfv(GL_LIGHT0, GL_POSITION, lightpos);

    shadowMap->preRender(lightpos[0], lightpos[1], lightpos[2]);
    geometry();
    shadowMap->postRender(GL_TEXTURE7);

    //main render
    GLuint shadowMapUniform = glGetUniformLocation(program, "shadowMap");
    GLuint shadowKernelSizeUniform = glGetUniformLocation(program, "shadowKernelSize");
    GLuint environmentMapUniform = glGetUniformLocation(program, "environmentMap");
    GLuint diffuseMapUniform = glGetUniformLocation(program, "diffuseMap");

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glCullFace(GL_BACK);

    glUseProgram(program);

    glUniform1i(shadowKernelSizeUniform, shadowKernelSize);

    glUniform1i(environmentMapUniform, 1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_CUBE_MAP, environmentMap->cubeMap);

    glUniform1i(diffuseMapUniform, 2);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_CUBE_MAP, diffuseMap->cubeMap);

    //use texture7 for the shadow map
    glUniform1i(shadowMapUniform, 7);
    glActiveTexture(GL_TEXTURE7);
    glBindTexture(GL_TEXTURE_2D, shadowMap->depthTexture);

	setPerspective();

    geometry();

	glutSwapBuffers();
}

void User::reshape(int w, int h) {
    // Prevent a divide by zero, when window is too short
	// (you cant make a window of zero width).
	if (h == 0) h = 1;

    this->h = h;
    this->w = w;
}

void User::keyboard(unsigned char key, int x, int y) {
    if (key == 'x') exit(0);
    if (key == 'w') eyeDist -= 0.25;
    if (key == 's') eyeDist += 0.25;
}

void User::mouseState(int button, int state, int x, int y) {
    if (state == GLUT_DOWN) {
        mouseButton = button;
        lastMouseX = x;
        lastMouseY = y;
    } else if (state == GLUT_UP) {
        mouseButton = -1;
    }
}

void User::mouseMotion(int x, int y) {
    if (mouseButton >= 0) {
        diffMouseX = x - lastMouseX;
        diffMouseY = y - lastMouseY;
    }
    lastMouseX = x;
    lastMouseY = y;
}
